Opalia · Private creator servers

Frontend architecture proposal

Route map, role model, component inventory and data contracts — for sign-off before any screen gets built. Built on your uploaded clean-room roleplay shell, styled only with design-system tokens so the palette stays an open decision.

Rev {{ revision }} Stack: Next.js app router · TS Theme: tokens only, palette TBD
01

Decisions taken from your answers

Hero journey

Fan: invite → apply → wait → approved → first session. Reads end-to-end in the prototype as one continuous click-path.

Full hi-fi first pass

Creator dashboard · Server home · Character card management · Extended roleplay page. Fan-flow screens ride along because the journey needs them.

Copy

One language at a time, switched from the header (EN / 中文) — never both on screen. Every string ships as an EN/ZH pair, so the switch is a data change, not a screen fork.

Roleplay layout

Immersive: messages float over the character art, exactly as your upload does. Rail, right panel and mobile drawer preserved.

Review layout (my call)

List + detail split on desktop, full-height drawer on mobile. Full trust & safety panel inside the detail column.

Gates

Invite code, creator questions, rules agreement, moderator approval, membership expiry, per-character grants — all mandatory. External account link becomes per-server optional.

Mock states

One states gallery screen holds every loading / empty / error / denied / expired / revoked variant, so product screens stay clean.

Colour

Light-blue accent, applied as a token retune only: the ramp is overridden at :root and every surface still reads var(--color-*). Swapping the theme stays a token edit, not a screen edit.

02

What the upload gives us, and what gets added

Kept from app/page.tsx + globals.css
ICON RAIL
topbar · identityprofile ⇄ chat
immersive scene · character art
composer (UI layer only)
detail panel

Three-column grid, panel open/closed/auto tri-state, profile ⇄ chat mode switch, 980px drawer breakpoint, 640px bottom-nav breakpoint, reduced-motion guard. All of it stays; the CSS variables at the top of globals.css are re-pointed at design-system tokens.

Added on top
  • Server switcher above the rail, with current-server indicator
  • Character switcher + session history in a second rail column
  • New session action; session list scoped to server + character
  • Membership access state — banner when expiring, lock when revoked
  • Model selector placeholder and conversation settings drawer
  • Access-denied variant of the whole shell for non-members
  • Composer split into Composer (view) + useSession() (future API seam)
Clean-room rule

No names, art, descriptions or messages carried over from any third-party product. Every character, server and message in the prototype is invented placeholder content, and art is a drawn placeholder rather than an image.

03

Route map

app router · route groups in parentheses

(public)

No session required
/ landing · value prop, invite-code field, approval explainer
/invite/[code] invitation landing · public-safe server facts only
/legal/[doc] terms, privacy, platform rules

(auth)

Carries ?next= so an invite link survives sign-in
/signin · /signup
/recover · /recover/[token]
/verify email/code confirmation
/link/[provider] generic external-account binding

(apply)

Signed in, not yet a member
/invite/[code]/apply the application form
/applications my applications
/applications/[id] status · 7 states

(member)

Approved membership required per server
/home across all joined servers
/s/[server] server home
/s/[server]/c/[char] character profile
/s/[server]/c/[char]/[session] roleplay
/sessions · /settings/[tab]

(studio)

Owner or moderator; capability-checked per action
/studio server picker + create
/studio/[server] overview dashboard
/studio/[server]/characters/[id]/[tab]
/studio/[server]/applications/[id]?
/studio/[server]/members · /invitations
/studio/[server]/announcements · /settings

(admin)

Platform administrator only
/admin/servers · /admin/creators
/admin/reports · /admin/audit

Out of scope for the first prototype pass — routes reserved so the role model stays complete.

04

Access gates

an invitation buys the right to apply, nothing more
01
Invitation

Code valid, unexpired, under application cap.

02
Platform account

Signed in and verified.

03
External account

Per-server setting — required, optional, or off. Generic provider slots.

04
Application

Creator questions answered, rules agreed, source recorded.

05
Approval

Owner or moderator decides. Then membership, group, expiry and per-character grants.

05

Role → navigation

nav is derived from capabilities, never hardcoded per page
Nav item Admin Owner Moderator Member Applicant Banned
Home 首页
My servers 我的服务器
Characters 角色granted only
Sessions 会话
Applications 申请read-only
Account settings 账号设置
Studio overview
Character cardsedit, no publish
Applications review
Membersno ban
Invitations
Announcements
Server settings
06

Component inventory

page content never owns styling; primitives never own domain logic
components/ui — primitives
ButtonIconButtonAvatarStatusBadgeTagFieldValidationMessageTextInputTextareaSelectCheckboxSegmentedTabsTablePaginationSearchFilterBarConfirmDialogSideDrawerToastSkeletonEmptyStateAccessDeniedBlueprintCoverPlaceholder
components/domain
ServerCardCharacterCardInvitationCardMemberRowApplicantRowApplicationDetailApplicationTimelineExternalAccountBadgeRiskSignalPanelGateChecklistReviewActionBarSessionRowAnnouncementBlockRulesListAccessMatrixMembershipBannerStatTileActivityFeed
components/shell + chat
AppShellIconRailServerSwitcherCharacterSwitcherMobileTabBarStudioShellStudioSidebarScreenHeaderRoleNavDetailPanelSceneStageMessageListMessageBubbleComposerModelSelectorConversationSettingsSessionHistory
07

Data contracts & mock service seam

type Role = 'admin' | 'owner' | 'moderator' | 'member'
          | 'applicant' | 'rejected' | 'banned'

type AppStatus = 'draft' | 'pending' | 'info_required'
               | 'approved' | 'rejected' | 'expired' | 'revoked'

interface Server {
  id, slug, name, tagline, coverRef
  creator: CreatorRef        // verified: boolean
  rules: string[]
  externalAccount: 'required' | 'optional' | 'off'
  questions: Question[]
  memberGroups: MemberGroup[]
}

interface Invitation {
  code, serverId, createdBy
  expiresAt, maxApplications, usedCount
  singleUse: boolean
  requiredProvider?: ProviderId
  defaultGroupId?: string
  disabled: boolean
}

interface Application {
  id, serverId, applicantId, invitationCode
  status: AppStatus
  displayName, reason, referrer?
  answers: Answer[]
  externalAccount?: ExternalAccountRef
  risk: RiskSignal[]          // server-computed
  notes: ReviewNote[]         // staff-only
  submittedAt, decidedAt?, decidedBy?
}

interface CharacterCard {
  id, serverId, name, blurb, avatarRef, coverRef
  tags: string[]
  visibility: 'draft' | 'published' | 'archived'
  groupIds: string[]
  version: number
  hasSystemPrompt: boolean    // never the prompt itself
  promptRef: string           // opaque server handle
  model: ModelSettings        // display-safe subset
}
Service layer

One lib/api/*.ts module per resource, each exporting async functions with the real signature and a mock implementation behind USE_MOCKS. Screens import the function, never the fixture. Swapping to real endpoints touches only these files.

Chat seam

Composer and MessageList are pure views over useSession(), which today appends to local state and returns a canned reply. A streaming transport drops in behind the same hook.

Prompt safety

System prompts, world info bodies and model keys never enter a client payload. The editor sends patches and shows a redacted summary — "12 entries · last edited 3d ago" — plus a character count.

08

Prototype build plan

one file, screen router, role switcher
A · fan journey

Landing → invitation → application → status (pending → approved) → enter server. Clickable straight through.

B · member

Server home with announcement, character collection, recent sessions, membership status.

C · roleplay

Your shell, extended: server + character switchers, sessions, model placeholder, settings drawer, working composer.

D · studio

Dashboard, character card editor with six form sections, review split view, invitations, members.

E · states

Gallery of all ten mock states plus the shared empty / skeleton / denied components.

Decide before I build
  • Studio nav: sidebar per server (my default) or a global sidebar with a server dropdown?
  • Does a member see other members at all, or is the roster creator-only?
  • Are sessions private to the fan, or can a creator inspect transcripts? Big trust question.
Deliberately out of scope this pass
  • Platform admin screens (routes reserved)
  • Payments, tipping, subscription tiers
  • Announcement composer, notification centre
  • Real auth, real chat transport, moderation pipeline